﻿// author : David Janacek, dj-soft@seznam.cz, 2013-12-13
// company: Asseco Solution, a.s. CZ
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Noris.Tools.QCode.QEncoder
{
    #region Interface for all QrMessageFormats classes
    /// <summary>
    /// Interface for all QrMessageFormats classes
    /// </summary>
    public interface IQrMessageFormats
    {
        /// <summary>
        /// Textual representation of the message
        /// </summary>
        string QrMessage { get; set; }
    }
    #endregion
    #region Ancestor class QrMessage - library of methods
    /// <summary>
    /// Optinal ancestor for any QrMessage classes. Contain library of protected methods.
    /// </summary>
    public class QrMessage
    {
        /// <summary>
        /// Return a text, where specified chars (listed in removeChars) are removed.
        /// </summary>
        /// <param name="text"></param>
        /// <param name="removeChars"></param>
        /// <returns></returns>
        protected string RemoveChars(string text, string removeChars)
        {
            if (text == null) return "";
            string result = "";
            foreach (char c in text)
            {
                if (removeChars.IndexOf(c) < 0)
                    result += c.ToString();
            }
            return result;
        }
        /// <summary>
        /// Return a text, where specified chars are replaced with chars from second string.
        /// Strings originalChars and replaceChars should be the same length:
        /// char X from originalChars (on position I) will be replaced with char Y from same index in replaceChars.
        /// If replaceChars will be shorter, then corresponding char in originalChars will be removed.
        /// </summary>
        /// <param name="text"></param>
        /// <param name="removeChars"></param>
        /// <returns></returns>
        protected string ReplaceChars(string text, string originalChars, string replaceChars)
        {
            if (text == null) return "";
            string result = "";
            foreach (char c in text)
            {
                int i = originalChars.IndexOf(c);
                if (i < 0)
                    // char c (in text) does not exists in string originalChars: no need to replace or remove:
                    result += c.ToString();
                else if (i >= 0 && i < replaceChars.Length)
                    // char c (in text) does exists in string originalChars, and on its index in string replaceChars exists a replacement:
                    result += replaceChars[i].ToString();
                // else: char c (in text) does exists in string originalChars, but not exists in string replaceChars: will be removed.
            }
            return result;
        }
        /// <summary>
        /// Return a text, containing only specified chars from original text. Filter out any other chars.
        /// </summary>
        /// <param name="text"></param>
        /// <param name="enabledChars"></param>
        /// <returns></returns>
        protected string KeepOnlyChars(string text, string enabledChars)
        {
            if (text == null) return "";
            string result = "";
            foreach (char c in text)
            {
                if (enabledChars.IndexOf(c) >= 0)
                    result += c.ToString();
            }
            return result;
        }
        /// <summary>
        /// Return value (number) formatted as string, with "." as decimal point, with specified decimal numbers (rounded).
        /// </summary>
        /// <param name="number"></param>
        /// <param name="decimalPoints"></param>
        /// <returns></returns>
        protected string NumToString(decimal number, int decimalPoints)
        {
            string mask = "################0";
            if (decimalPoints > 0)
                mask += ".".PadRight(decimalPoints, '0');
            decimal value = Math.Round(number, decimalPoints);
            string result = value.ToString(mask);
            result = result.Replace(",", ".");
            return result;
        }
    }
    #endregion
    #region Message Url
    /// <summary>
    /// Website URL
    /// </summary>
    public class QrMessageUrl : QrMessage, IQrMessageFormats
    {
        public string UrlAdress { get; set; }
        string IQrMessageFormats.QrMessage
        {
            get
            {
                string message = "";
                if (!String.IsNullOrEmpty(this.UrlAdress))
                    message += this.UrlAdress;
                return message;
            }
            set
            { }
        }
    }
    #endregion
    #region Message Phone number
    /// <summary>
    /// Pre-Populated Telephone Number
    /// </summary>
    public class QrMessagePhoneNumber : QrMessage, IQrMessageFormats
    {
        public string PhoneNumber { get; set; }
        string IQrMessageFormats.QrMessage
        {
            get
            {
                string message = "tel";
                if (!String.IsNullOrEmpty(this.PhoneNumber))
                    message += ":" + KeepOnlyChars(this.PhoneNumber, "0123456789");
                return message;
            }
            set
            { }
        }
    }
    #endregion
    #region Message MailTo
    /// <summary>
    /// Pre-populated email address and content
    /// </summary>
    public class QrMessageMailTo : QrMessage, IQrMessageFormats
    {
        public string MailAdress { get; set; }
        public string MailSubject { get; set; }
        public string MailContent { get; set; }
        string IQrMessageFormats.QrMessage
        {
            get
            {
                // MATMSG:TO:adress;SUB:subject;BODY:content;;
                string message = "MATMSG:";
                if (!String.IsNullOrEmpty(this.MailAdress))
                    message += "TO:" + RemoveChars(this.MailAdress, ":;") + ";";
                if (!String.IsNullOrEmpty(this.MailSubject))
                    message += "SUB:" + RemoveChars(this.MailSubject, ":;") + ";";
                if (!String.IsNullOrEmpty(this.MailContent))
                    message += "BODY:" + this.MailContent + ";;";
                return message;
            }
            set
            { }
        }
    }
    #endregion
    #region Message SmsTo
    /// <summary>
    /// Pre-populated SMS Message
    /// </summary>
    public class QrMessageSmsTo : QrMessage, IQrMessageFormats
    {
        public string SmsNumber { get; set; }
        public string SmsContent { get; set; }
        string IQrMessageFormats.QrMessage
        {
            get
            {
                string message = "smsto";
                if (!String.IsNullOrEmpty(this.SmsNumber))
                    message += ":" + RemoveChars(this.SmsNumber, ":");
                if (!String.IsNullOrEmpty(this.SmsContent))
                    message += ":" + RemoveChars(this.SmsContent, ":");
                return message;
            }
            set
            { }
        }
    }
    #endregion
    #region Message MmsTo
    /// <summary>
    /// Pre-populated MMS Message
    /// </summary>
    public class QrMessageMmsTo : QrMessage, IQrMessageFormats
    {
        public string MmsNumber { get; set; }
        public string MmsContent { get; set; }
        string IQrMessageFormats.QrMessage
        {
            get
            {
                string message = "mms";
                if (!String.IsNullOrEmpty(this.MmsNumber))
                    message += ":" + RemoveChars(this.MmsNumber, ":");
                if (!String.IsNullOrEmpty(this.MmsContent))
                    message += ":" + RemoveChars(this.MmsContent, ":");
                return message;
            }
            set
            { }
        }
    }
    #endregion
    #region Message GpsLocation
    /// <summary>
    /// GPS location:
    /// latitude, longitude, altitude in meters (a minus sign denotes a South latitude and a West longitude)code for Google offices
    /// </summary>
    public class QrMessageGpsLocation : QrMessage, IQrMessageFormats
    {
        /// <summary>
        /// Latitude in degress: positive for North, negative for South
        /// </summary>
        public decimal GpsLatitude { get; set; }
        /// <summary>
        /// Longitude in degress: positive for East, negative for West
        /// </summary>
        public decimal GpsLongitude { get; set; }
        /// <summary>
        /// Altitude above sea, in meters
        /// </summary>
        public decimal GpsAltitude { get; set; }
        string IQrMessageFormats.QrMessage
        {
            get
            {
                string message = 
                    NumToString(this.GpsLatitude, 5) + "," +
                    NumToString(this.GpsLongitude, 5) + "," +
                    NumToString(this.GpsAltitude, 0);
                return message;
            }
            set
            { }
        }
    }
    #endregion
    #region Message Contact Information
    /// <summary>
    /// Contact Information
    /// </summary>
    public class QrMessageContactInfo : QrMessage, IQrMessageFormats
    {
        public QrMessageContactInfo()
        {
            this.ContactFormat = QrMessageContactFormat.Mecard2;
        }
        public QrMessageContactFormat ContactFormat { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string PostCode { get; set; }
        public string PhoneNumber { get; set; }
        public string EMail { get; set; }
        string IQrMessageFormats.QrMessage
        {
            get
            {
                switch (this.ContactFormat)
                {
                    case QrMessageContactFormat.VCard2: return this.GetVCard2();
                    case QrMessageContactFormat.Mecard1: return this.GetMecard1();
                    case QrMessageContactFormat.Mecard2: return this.GetMecard2();
                }
                return this.GetMecard2();
            }
            set
            { }
        }
        protected string GetVCard2()
        {
            string eol = Environment.NewLine;
            string message = "BEGIN:VCARD" + eol + "VERSION:2.1" + eol;
            if (!String.IsNullOrEmpty(this.LastName))
                message += RemoveChars(this.LastName, ",;:\r\n");
            message += ",";
            if (!String.IsNullOrEmpty(this.FirstName))
                message += ":" + RemoveChars(this.FirstName, ",;:\r\n");
            message += ";ADR:";
            if (!String.IsNullOrEmpty(this.AddressLine1))
                message += ":" + RemoveChars(this.AddressLine1, ",;:\r\n");
            message += ",";
            if (!String.IsNullOrEmpty(this.AddressLine2))
                message += ":" + RemoveChars(this.AddressLine2, ",;:\r\n");
            message += ",";
            if (!String.IsNullOrEmpty(this.City))
                message += ":" + RemoveChars(this.City, ",;:\r\n");
            message += ",ST";
            if (!String.IsNullOrEmpty(this.PostCode))
                message += ":" + KeepOnlyChars(this.PostCode, "0123456789");
            message += ";TEL:+";
            if (!String.IsNullOrEmpty(this.PhoneNumber))
                message += ":" + KeepOnlyChars(this.PhoneNumber, "0123456789");
            message += ";EMAIL:";
            if (!String.IsNullOrEmpty(this.EMail))
                message += ":" + RemoveChars(this.EMail, ",;:\r\n");
            message += "END:VCARD";
            return message;

            //BEGIN:VCARD
            //VERSION:2.1
            //N:Gump;Forrest
            //FN:Forrest Gump
            //ORG:Bubba Gump Shrimp Co.
            //TITLE:Shrimp Man
            //PHOTO;GIF:http://www.example.com/dir_photos/my_photo.gif
            //TEL;WORK;VOICE:(111) 555-1212
            //TEL;HOME;VOICE:(404) 555-1212
            //ADR;WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
            //LABEL;WORK;ENCODING=QUOTED-PRINTABLE:100 Waters Edge=0D=0ABaytown, LA 30314=0D=0AUnited States of America
            //ADR;HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
            //LABEL;HOME;ENCODING=QUOTED-PRINTABLE:42 Plantation St.=0D=0ABaytown, LA 30314=0D=0AUnited States of America
            //EMAIL;PREF;INTERNET:forrestgump@example.com
            //REV:20080424T195243Z
            //END:VCARD
        }
        protected string GetMecard1()
        {
            //The field names used are:
            //N:  Contact name (if comma separated, uses LASTNAME,FIRSTNAME)
            //SOUND:  Contact name in Japanese (Kana). Same comma rules as N:
            //TEL: Telephone number
            //TEL-AV: Videophone Tel number
            //EMAIL:  Email address
            //NOTE: Memo text field
            //BDAY: Birthday (YYYMMDD format)
            //ADR: Address. Comma separated: PO box, room number, house number, city, prefecture, zip code and country
            //URL:  Website
            //NICKNAME: Display name
            string message = "MECARD:";
            
            bool n1 = !String.IsNullOrEmpty(this.LastName);
            bool n2 = !String.IsNullOrEmpty(this.FirstName);
            if (n1 && n2)
                message += "N:" + RemoveChars(this.LastName, ",;:\r\n") + "," + RemoveChars(this.FirstName, ",;:\r\n") + ";";
            else if (n1)
                message += "N:" + RemoveChars(this.LastName, ",;:\r\n") + ";";
            else if (n2)
                message += "N:" + RemoveChars(this.FirstName, ",;:\r\n") + ";";

            bool a1 = !String.IsNullOrEmpty(this.AddressLine1);
            bool a2 = !String.IsNullOrEmpty(this.AddressLine2);
            bool a3 = !String.IsNullOrEmpty(this.City);
            bool a4 = !String.IsNullOrEmpty(this.PostCode);
            if (a1 || a2 || a3 || a4)
            {
                message += "ADR:" + 
                    "," +               // PO box,
                    "," +               // Room number,
                    (a1 ? RemoveChars(this.AddressLine1, ",;:\r\n") : "") + "," +   // house number, 
                    (a2 ? RemoveChars(this.AddressLine2, ",;:\r\n") : "") + "," +   // city,
                    (a3 ? RemoveChars(this.City, ",;:\r\n") : "") + "," +           // prefecture,
                    (a4 ? RemoveChars(this.PostCode, ",;:\r\n") : "") + "," +       // zip code,
                    ";";                                                            // country;
            }

            if (!String.IsNullOrEmpty(this.PhoneNumber))
                message += "TEL:+" + KeepOnlyChars(this.PhoneNumber, "0123456789") + ";";

            if (!String.IsNullOrEmpty(this.EMail))
                message += "EMAIL:" + RemoveChars(this.EMail, ",;:\r\n") + ";";
            
            return message;
        }
        protected string GetMecard2()
        {
            //The field names used are:
            //N:  Contact name (if comma separated, uses LASTNAME,FIRSTNAME)
            //SOUND:  Contact name in Japanese (Kana). Same comma rules as N:
            //TEL: Telephone number
            //TEL-AV: Videophone Tel number
            //EMAIL:  Email address
            //NOTE: Memo text field
            //BDAY: Birthday (YYYMMDD format)
            //ADR: Address. Comma separated: PO box, room number, house number, city, prefecture, zip code and country
            //URL:  Website
            //NICKNAME: Display name
            string message = "MECARD:";

            bool n1 = !String.IsNullOrEmpty(this.LastName);
            bool n2 = !String.IsNullOrEmpty(this.FirstName);
            if (n1 && n2)
                message += "N:" + RemoveChars(this.LastName, ",;:\r\n") + "," + RemoveChars(this.FirstName, ",;:\r\n") + ";";
            else if (n1)
                message += "N:" + RemoveChars(this.LastName, ",;:\r\n") + ";";
            else if (n2)
                message += "N:" + RemoveChars(this.FirstName, ",;:\r\n") + ";";

            bool a1 = !String.IsNullOrEmpty(this.AddressLine1);
            bool a2 = !String.IsNullOrEmpty(this.AddressLine2);
            bool a3 = !String.IsNullOrEmpty(this.City);
            bool a4 = !String.IsNullOrEmpty(this.PostCode);
            if (a1 || a2 || a3 || a4)
            {
                message += "ADR:" +
                    (a1 ? RemoveChars(this.AddressLine1, ",;:\r\n") + "," : "") +
                    (a2 ? RemoveChars(this.AddressLine2, ",;:\r\n") + "," : "") +
                    (a3 ? RemoveChars(this.City, ",;:\r\n") + "," : "") +
                    (a4 ? RemoveChars(this.PostCode, ",;:\r\n") + "," : "") +
                    ";";
            }

            if (!String.IsNullOrEmpty(this.PhoneNumber))
                message += "TEL:" + KeepOnlyChars(this.PhoneNumber, "0123456789") + ";";

            if (!String.IsNullOrEmpty(this.EMail))
                message += "EMAIL:" + RemoveChars(this.EMail, ",;:\r\n") + ";";

            return message;
        }
    }
    public enum QrMessageContactFormat
    {
        None = 0,
        VCard2,
        Mecard1,
        Mecard2
    }
    #endregion
    #region Message CzBankSpd Common
    /// <summary>
    /// Formát pro sdílení platebních údajů v rámci tuzemského platebního styku v CZK prostřednictvím QR kódů.
    /// Všeobecný formát, který akceptují veškeré CZ banky (viz tabulka 3, Standard ČBA – Formát pro sdílení platebních údajů ..., Verze 1 – listopad 2012).
    /// Obsahuje podmnožinu atributů. Kompletní formát je implementován ve třídě QrMessageCzBankSpdComplete.
    /// </summary>
    public class QrMessageCzBankSpdCommon : QrMessage, IQrMessageFormats
    {
        #region Property
        /// <summary>
        /// ACC:
        /// Identifikace protistrany, která je složena ze dvou komponent oddělených znaménkem +
        /// Tyto komponenty jsou: číslo účtu ve formátu IBAN identifikace banky ve formátu SWIFT dle ISO 9362.
        /// Druhá komponenta (SWIFT) je přitom volitelná.
        /// Max. 46 znaků (IBAN+BIC)
        /// </summary>
        public string Account { get; set; }
        /// <summary>
        /// ALT-ACC:
        /// Seznam alternativních účtů k výchozímu účtu danému hodnotou ACC.
        /// Jednotlivé záznamy mají stejný formát jako pole ACC.
        /// Klientské aplikace mohou tento seznam použít pro zobrazení volby, na který účet se má platba odeslat, např. za účelem optimalizace výše poplatků za převod.
        /// Pro potřebu zachování akceptovatelné datové hustoty dat na mediu typu QR kód se do pole nedoporučuje vkládat více než 2 alternativní čísla účtu.
        /// </summary>
        public string[] AlterAccounts { get; set; }
        /// <summary>
        /// AM:
        /// Výše částky platby.
        /// Max 9 999 999,99
        /// </summary>
        public Decimal? Amount { get; set; }
        /// <summary>
        /// CC:
        /// Měna platby.
        /// Právě 3 znaky.
        /// </summary>
        public string CurrencyCode { get; set; }
        /// <summary>
        /// DT:
        /// Datum splatnosti.
        /// Přebírá se pouze datová část, ne čas.
        /// </summary>
        public DateTime? DueDate { get; set; }
        /// <summary>
        /// X-VS:
        /// Variabilní symbol.
        /// Max. 10 znaků, pouze čísla a mezery.
        /// </summary>
        public string VariableSymbol { get; set; }
        /// <summary>
        /// X-SS:
        /// Specifický symbol.
        /// Max. 10 znaků, pouze čísla a mezery.
        /// </summary>
        public string SpecificSymbol { get; set; }
        /// <summary>
        /// X-KS:
        /// Konstantní symbol.
        /// Max. 10 znaků, pouze čísla a mezery.
        /// </summary>
        public string ConstantSymbol { get; set; }
        /// <summary>
        /// MSG:
        /// Zpráva pro příjemce.
        /// Max. 60 znaků, všechny znaky z povolené množiny mimo *
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// Sada specifických atributů.
        /// Norma připouští přidávání dalších údajů po dohodě s konkrétní bankou.
        /// Pokud bude přidán klíč, který je obsažen v základní definici, a jeho hodnota je v odpovídající property naplněna, dojde k chybě (při vyhodnocení QR kódu).
        /// Pokud ale bude přidán klíč shodný se standardním klíčem, jehož hodnota (v property) naplněna není, pak k chybě nedojde, 
        /// do výsledného kódu bude vepsána hodnota ze seznamu SpecificAttribute, a na rozdíl od standardního atributu se nebude provádět kontrola správnosti podle předpisu pro standardní klíč.
        /// </summary>
        public List<KeyValuePair<string, string>> SpecificAttribute { get; set; }
        /// <summary>
        /// Použít zabezpečení pomocí CRC32? Hodnota bude dopočítána automaticky.
        /// </summary>
        public bool UseCrc32 { get; set; }
        #endregion
        #region Tvorba QrMessage
        /// <summary>
        /// IQrMessageFormats.QrMessage
        /// </summary>
        string IQrMessageFormats.QrMessage { get { return this._GetQrMessage(); } set { } }
        private string _GetQrMessage()
        {
            this.KeyUsedDict = new Dictionary<string, SpdKeyValue>();
            this.AddSpdKeyValues();             // Virtuální metoda: přidá standardní atributy [+atributy potomků]
            this.AddSpecificAttributes();       // Přidá atributy z this.SpecificAttribute
            string message = GetSpdMessage();
            return message;
        }
        /// <summary>
        /// Připraví do bufferu všechny svoje standardní klíče.
        /// </summary>
        /// <returns></returns>
        protected virtual void AddSpdKeyValues()
        {
            this.AddToCode("ACC", this.Account, 46, true);
            this.AddToCode("ACC-ALT", this.AlterAccounts, 46, ",", false);
            this.AddToCode("AM", this.Amount, false);
            this.AddToCode("CC", this.CurrencyCode, 3, false);
            this.AddToCode("DT", this.DueDate, false);
            this.AddToCode("MSG", this.Message, 60, false);
            this.AddToCode("X-VS", this.VariableSymbol, 10, "0123456789", false);
            this.AddToCode("X-SS", this.SpecificSymbol, 10, "0123456789", false);
            this.AddToCode("X-KS", this.ConstantSymbol, 10, "0123456789", false);
        }
        /// <summary>
        /// Přidá klíče a hodnoty pro specifické atributy (this.SpecificAttribute).
        /// Volá se až po skončení základní metody AddSpdKeyValues(), proto aby specifické atributy nepřekážely základním atributům.
        /// </summary>
        protected void AddSpecificAttributes()
        {
            if (this.SpecificAttribute != null)
            {
                foreach (KeyValuePair<string, string> spa in this.SpecificAttribute)
                    this.AddToCode(spa.Key, spa.Value, null, false);
            }
        }
        /// <summary>
        /// Sestaví zprávu z hlavičky + všechny klíče a hodnoty.
        /// Všechny použité atriuty jsou připraveny v this.KeyUsedDict.
        /// Metoda this.PrepareAttributeList() vrátí setříděný seznam atributů z této Dictionary.
        /// </summary>
        /// <returns></returns>
        protected virtual string GetSpdMessage()
        {
            string message = "SPD*1.0*";
            List<SpdKeyValue> list = this.PrepareAttributeList();
            foreach (SpdKeyValue spd in list)
                message += spd.FormattedKeyValue;
            if (this.UseCrc32)
                message += GetCrc32(message);                        // CRC32 se počítá včetně hvězdičky na konci, CRC32 přidává na konec svoji hvězdičku
            message = message.Substring(0, message.Length - 1);      // Odstranit koncovou hvězdičku
            return message;
        }
        /// <summary>
        /// Vypočte a vrátí CRC32 pro daný text
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        protected string GetCrc32(string message)
        {
            uint crc32 = Noris.Tools.QCode.QEncoder.Crc32.Compute(System.Text.Encoding.UTF8.GetBytes(message));
            string attr = "CRC32:" + crc32.ToString("X8").ToUpper() + "*";
            return attr;
        }
        /// <summary>
        /// Vrátí seznam použitých atributů pro sestavení zprávy.
        /// Seznam je v případě potřeby (this.UseCrc32) setříděn podle normy (atributy se setřídí dle klíče a sekundárně dle hodnoty).
        /// </summary>
        /// <returns></returns>
        protected List<SpdKeyValue> PrepareAttributeList()
        {
            List<SpdKeyValue> list = this.KeyUsedDict.Values.ToList();
            if (this.UseCrc32)
                list.Sort(SpdKeyValue.CompareByKeyValue);
            return list;
        }
        #endregion
        #region Podpora pro vkládání klíče a hodnoty
        /// <summary>
        /// Přidá klíč a desetinné číslo
        /// </summary>
        /// <param name="key"></param>
        /// <param name="number"></param>
        /// <param name="isLiable"></param>
        protected void AddToCode(string key, Int32? number, bool isLiable)
        {
            string text = "";
            if (number.HasValue)
                text = number.Value.ToString().Trim();
            this.AddToCode(key, text, null, isLiable);
        }
        /// <summary>
        /// Přidá klíč a desetinné číslo
        /// </summary>
        /// <param name="key"></param>
        /// <param name="number"></param>
        /// <param name="isLiable"></param>
        protected void AddToCode(string key, decimal? number, bool isLiable)
        {
            string text = "";
            if (number.HasValue)
                text = number.Value.ToString("########0.00").Trim().Replace(",", ".");
            this.AddToCode(key, text, null, isLiable);
        }
        /// <summary>
        /// Přidá klíč a datum
        /// </summary>
        /// <param name="key"></param>
        /// <param name="date"></param>
        /// <param name="isLiable"></param>
        protected void AddToCode(string key, DateTime? date, bool isLiable)
        {
            string text = "";
            if (date.HasValue)
                text = date.Value.ToString("yyyyMMdd");
            this.AddToCode(key, text, null, isLiable);
        }
        /// <summary>
        /// Přidá klíč a sadu textových hodnot oddělených oddělovačem.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="values"></param>
        /// <param name="maxLength"></param>
        /// <param name="delimiter"></param>
        /// <param name="isLiable"></param>
        protected void AddToCode(string key, string[] values, Int32? maxLength, string delimiter, bool isLiable)
        {
            string text = "";
            if (values != null)
            {
                foreach (string value in values)
                {
                    CheckMaxLength(key, value, maxLength);
                    text += delimiter + value;
                }
                if (text.Length >= delimiter.Length)
                    text = text.Substring(delimiter.Length);
            }
            this.AddToCode(key, text, null, isLiable);
        }
        /// <summary>
        /// Přidá klíč a hodnotu, hodnotu ověří zda nepřekračuje danou délku (před formátováním).
        /// Pokud je hodnota prázdná, ale klíč je povinný, vyhodí chybu.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="text"></param>
        /// <param name="maxLength"></param>
        /// <param name="isLiable"></param>
        protected void AddToCode(string key, string text, Int32? maxLength, bool isLiable)
        {
            this.AddToCode(key, text, maxLength, null, isLiable);
        }
        /// <summary>
        /// Přidá klíč a hodnotu, hodnotu ověří zda nepřekračuje danou délku (před formátováním).
        /// Pokud je hodnota prázdná, ale klíč je povinný, vyhodí chybu.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="text"></param>
        /// <param name="maxLength"></param>
        /// <param name="isLiable"></param>
        protected void AddToCode(string key, string text, Int32? maxLength, string enabledChars, bool isLiable)
        {
            if (String.IsNullOrEmpty(key))
                throw new InvalidProgramException("Klíč pro metodu AddToCode() je prázdný.");

            if (String.IsNullOrEmpty(text))
            {
                if (isLiable)
                    throw new FormatException("Klíč " + key + " je povinný, jeho hodnota není vyplněna.");
                return;
            }
            if (this.KeyUsedDict.ContainsKey(key))
                throw new FormatException("Klíč " + key + " je použit více než jedenkrát.");

            string formattedValue = text;
            if (enabledChars != null)
                KeepOnlyChars(formattedValue, enabledChars);
            formattedValue = ToMsgValue(formattedValue);

            this.KeyUsedDict.Add(key, new SpdKeyValue(key, formattedValue.Trim()));
        }
        /// <summary>
        /// Ověří, zda hodnota (value) nepřesahuje stanovenou délku.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="maxLength"></param>
        protected static void CheckMaxLength(string key, string value, Int32? maxLength)
        {
            if (maxLength.HasValue && value != null && value.Trim().Length > maxLength.Value)
                throw new FormatException("Hodnota pro klíč " + key + " je delší, než je povoleno: \"" + value + "\", délka=" + value.Length.ToString() + ", maximum=" + maxLength.Value.ToString() + ".");
        }
        /// <summary>
        /// Vrátí hodnotu stringu upravenou do formátu SPD
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        protected static string ToMsgValue(string value)
        {
            string formattedValue = value
                .Replace("*", "%2A")
                .Replace("\r", "")
                .Replace("\n", "");
            return formattedValue;
        }
        /// <summary>
        /// Dictionary with KeyValues in process creating IQrMessageFormats.QrMessage (get)
        /// </summary>
        protected Dictionary<string, SpdKeyValue> KeyUsedDict;
        /// <summary>
        /// SPD format Key and Value
        /// </summary>
        protected class SpdKeyValue
        {
            internal SpdKeyValue(string key, string value)
            {
                this.Key = key;
                this.Value = value;
            }
            public override string ToString()
            {
                return this.FormattedKeyValue;
            }
            /// <summary>
            /// Klíč hodnoty, bez oddělovače ":"
            /// </summary>
            public string Key { get; private set; }
            /// <summary>
            /// Hodnota už zformátovaná do zprávy, bez oddělovače "*"
            /// </summary>
            public string Value { get; private set; }
            /// <summary>
            /// Klíč a hodnota včetně oddělovačů: DT:20120524*
            /// </summary>
            public string FormattedKeyValue { get { return this.Key + ":" + this.Value + "*"; } }
            /// <summary>
            /// Komparátor dle normy ČNB: atributy se setřídí dle klíče a sekundárně dle hodnoty
            /// </summary>
            /// <param name="a"></param>
            /// <param name="b"></param>
            /// <returns></returns>
            public static int CompareByKeyValue(SpdKeyValue a, SpdKeyValue b)
            {
                return a.FormattedKeyValue.CompareTo(b.FormattedKeyValue);
            }
        }
        #endregion
    }
    #endregion
    #region Message CzBankSpd Complete
    /// <summary>
    /// Formát pro sdílení platebních údajů v rámci tuzemského
    /// platebního styku v CZK prostřednictvím QR kódů.
    /// Všeobecný formát, který akceptují veškeré CZ banky (viz tabulka 3, Standard ČBA – Formát pro sdílení platebních údajů ..., Verze 1 – listopad 2012)
    /// </summary>
    public class QrMessageCzBankSpdComplete : QrMessageCzBankSpdCommon, IQrMessageFormats
    {
        #region Property
        /// <summary>
        /// RF:
        /// Identifikátor platby pro příjemce.
        /// Max. 16 znaků, Celé číslo.
        /// </summary>
        public string RecipientIdent { get; set; }
        /// <summary>
        /// RN:
        /// Jméno příjemce.
        /// Max. 35 znaků, všechny znaky z povolené množiny mimo *
        /// </summary>
        public string RecipientName { get; set; }
        /// <summary>
        /// PT:
        /// Typ platby.
        /// Max. 3 znaky, všechny znaky z povolené množiny mimo *
        /// </summary>
        public string PaymentType { get; set; }
        /// <summary>
        /// NT:
        /// Identifikace kanálu pro zaslání notifikace výstavci platby.
        /// Poznámka: odesílání notifikací se řídí podle podmínek jednotlivých bank, nicméně doporučuje se odesílat v okamžiku blokace prostředků na účtu plátce.
        /// </summary>
        public QrMessageSpdNotificationType NotificationType { get; set; }
        /// <summary>
        /// NTA:
        /// Telefonní číslo v mezinárodním nebo lokálním vyjádření nebo E-mailová adresa.
        /// Max. 320 znaků
        /// </summary>
        public string NotificationAdress { get; set; }
        /// <summary>
        /// X-PER:
        /// Počet dní, po které se má provádět pokus o opětovné provedení neúspěšné platby (z důvodů např. nedostupných prostředků na účtu příkazce).
        /// Celé číslo, Max. hodnota 30
        /// </summary>
        public Int32? RepeatPeriod { get; set; }
        /// <summary>
        /// X-ID:
        /// Identifikátor platby na straně příkazce. Jedná se o interní ID, jehož použití a interpretace závisí na bance příkazce.
        /// Může být použito například jako identifikace e-commerce platby, nebo pro statistické či marketingové účely.
        /// Max. 20 znaků. Všechny znaky z povolené množiny mimo *
        /// </summary>
        public string PayerIdent { get; set; }
        /// <summary>
        /// X-URL:
        /// URL, které je možno využít pro vlastní potřebu.
        /// Max. 140 znaků. Všechny znaky z povolené množiny mimo *
        /// </summary>
        public string PayerUrl { get; set; }
        #endregion
        #region Tvorba QrMessage
        /// <summary>
        /// Připraví do bufferu všechny svoje standardní klíče.
        /// </summary>
        protected override void AddSpdKeyValues()
        {
            base.AddSpdKeyValues();
            this.AddToCode("RF", this.RecipientIdent, 16, "0123456789", false);
            this.AddToCode("RN", this.RecipientName, 35, false);
            this.AddToCode("PT", this.PaymentType, 3, false);
            this.AddToCode("NT", this.NotificationTypeString, 1, false);
            this.AddToCode("NTA", this.NotificationAdress, 320, false);
            this.AddToCode("X-PER", this.RepeatPeriod, false);
            this.AddToCode("X-ID", this.PayerIdent, 20, false);
            this.AddToCode("X-URL", this.PayerUrl, 140, false);
        }
        /// <summary>
        /// Typ notifikace písmenem podle normy (P/E/null)
        /// </summary>
        protected string NotificationTypeString
        {
            get
            {
                switch (this.NotificationType)
                {
                    case QrMessageSpdNotificationType.Phone: return "P";
                    case QrMessageSpdNotificationType.Email: return "E";
                }
                return null;
            }
        }
        #endregion
    }
    /// <summary>
    /// Identifikace kanálu pro zaslání notifikace výstavci platby.
    /// </summary>
    public enum QrMessageSpdNotificationType
    {
        /// <summary>
        /// Nezadávat
        /// </summary>
        None = 0,
        /// <summary>
        /// P - notifikace bude zaslána formou SMS
        /// </summary>
        Phone,
        /// <summary>
        /// E - notifikace bude zaslána formou e-mailu
        /// </summary>
        Email
    }

    #endregion
}
